﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Threading;
using System.Linq;

namespace NielsDerdaele.HttpSocket.Phone {
    public class HttpClientSocket {
        #region Variables
        private string uid, hostname;
        private int port, id=0;
        //private List<HttpWebRequest> callbacks = new List<HttpWebRequest>();
        private byte[] remainingData;
        private Queue<byte[]> dataQueue = new Queue<byte[]>();        
        private object idLock = new object();
        private List<byte> writedata = new List<byte>();
        private ManualResetEvent readEvent = new ManualResetEvent(true);
        #endregion

        #region Events
        public event EventHandler<AsyncCompletedEventArgs> ConnectCompleted;
        #endregion

        protected virtual void OnConnectCompleted(Exception e) {
            if (ConnectCompleted != null) {
                ConnectCompleted(this, new AsyncCompletedEventArgs(e, false, null));
            }
        }        
        protected virtual void OnDataReceived(byte[] data) {
            lock (dataQueue) {
                dataQueue.Enqueue(data); //voeg data achteraan de queue toe
                readEvent.Set();
            }
        }

        public void BeginConnect(string hostname, int port) {
            var req = HttpWebRequest.CreateHttp("http://" + hostname + ":" + port + "/?type=uid");
            var result = req.BeginGetResponse(new AsyncCallback(endConnect), req);
            this.hostname = hostname;
            this.port = port;
        }
        private void endConnect(IAsyncResult result) {
            try {
                var req = (HttpWebRequest)result.AsyncState;
                var res = req.EndGetResponse(result);
                var sr = new StreamReader(res.GetResponseStream(), Encoding.UTF8);
                var uid = sr.ReadToEnd();
                res.Close();

                if (String.IsNullOrEmpty(uid)) { //mislukt!
                    OnConnectCompleted(new Exception("Couldn't establish connection"));
                } else { //gelukt!
                    this.uid = uid;
                    openNewCallbackChannel();
                    OnConnectCompleted(null);
                }
            } catch (Exception e) {
                OnConnectCompleted(e);
            }
        }

        /// <summary>
        /// Send a new HTTP request that we will use as callback channel
        /// </summary>
        private void openNewCallbackChannel() {
            new Thread(new ThreadStart(() => {
                var req = HttpWebRequest.CreateHttp("http://" + hostname + ":" + port + "?type=callback&uid=" + uid);              
                req.BeginGetResponse(new AsyncCallback(callbackData), req);
            })).Start();
        }

        /// <summary>
        /// Function that's called when data is received on a callback channel (HTTP response)
        /// </summary>
        /// <param name="result"></param>
        private void callbackData(IAsyncResult result) {
            var req = (HttpWebRequest)result.AsyncState;            
            var res = req.EndGetResponse(result);
            Stream stream = res.GetResponseStream();             
            var data = stream.ReadToArray(); //lees alle data in deze stream naar een byte array
            OnDataReceived(data);
            openNewCallbackChannel();
        }
        
        public void Flush() {
            HttpWebRequest req;
            lock (idLock) {
                req = HttpWebRequest.CreateHttp("http://" + hostname + ":" + port + "/?type=data&uid=" + uid + "&id=" + id++);
            }
            req.Method = "POST";
            //TODO: use data request as callback channel
            req.BeginGetRequestStream(new AsyncCallback((x) => { //send the data!
                var request = (HttpWebRequest)req;
                var stream = request.EndGetRequestStream(x);
                byte[] buffer;
                lock (writedata) {
                    buffer = writedata.ToArray();                    
                    writedata.Clear();
                }
                
                stream.Write(buffer, 0, buffer.Length);                
                stream.Close();
                req.BeginGetResponse(new AsyncCallback((y) => { }), null);
            }), req);
        }

        /// <summary>
        /// Write count elements
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="offset"></param>
        /// <param name="count"></param>
        public void Write(byte[] buffer, int offset, int count) {
            writedata.AddRange(buffer.Skip(offset).Take(count));            
        }

        public int Read(byte[] buffer, int offset, int count) {            
            int bytesRead = 0;

            #region Read RemainingData Array
            if (remainingData != null) { //There's a block that has been partially read
                if (count >= remainingData.Length) { //we can read the entire block
                    Array.Copy(remainingData, 0, buffer, offset, remainingData.Length);                    
                    bytesRead = remainingData.Length;
                    remainingData = null;
                } else {
                    Array.Copy(remainingData, 0, buffer, offset + bytesRead, count); //we may only read count elements
                    var temp = new byte[remainingData.Length - count];
                    Array.Copy(remainingData, 0 + count, temp, 0, temp.Length); //copy the remaining bytes to a new array
                    remainingData = temp; //store this array
                    return count;
                }
            }
            #endregion

            if (bytesRead == 0) { 
                lock (dataQueue) {
                    if (dataQueue.Count == 0) {
                        readEvent.Reset();
                    }
                }
            }
            readEvent.WaitOne();//wait on new data (if none was read)
                        
            while (bytesRead < count && dataQueue.Count > 0) { //We have items remaining in the queue
                byte[] data;
                lock (dataQueue) {
                     data = dataQueue.Dequeue(); //Dequeue the first item
                }
                if (count - bytesRead > data.Length) { //We can copy the entire array
                    Array.Copy(data, 0, buffer, offset + bytesRead, data.Length);
                    bytesRead += data.Length;
                } else { //we can only copy count - bytesRead bytes from the array
                    Array.Copy(data, 0, buffer, offset + bytesRead, count - bytesRead);
                    var temp = new byte[data.Length - (count-bytesRead)];
                    Array.Copy(data, 0 + (count - bytesRead), temp, 0, temp.Length);
                    remainingData = temp;
                    return count;
                }
            }
            return bytesRead;
        }
    }
}
